package util

import (
	"context"
	"errors"
	"fmt"
	"github.com/aws/aws-sdk-go-v2/aws"
	v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/credentials"
	"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/aws/aws-sdk-go-v2/service/s3/types"
	"github.com/aws/smithy-go"
	redis2 "github.com/gomodule/redigo/redis"
	"io"
	"mime"
	"path/filepath"
	"time"
)

type OSS struct {
	Client  *s3.Client
	PClient *s3.PresignClient
}

func NewOSS(accessKey, secretKey, endpoint, region string) (*OSS, error) {
	if region == "" {
		region = "us-east-1"
	}
	cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region))
	if err != nil {
		return nil, err
	}
	client := s3.NewFromConfig(cfg, func(o *s3.Options) {
		o.Credentials = aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(accessKey, secretKey, ""))
		o.BaseEndpoint = aws.String(endpoint)
	})
	pClient := s3.NewPresignClient(client)
	return &OSS{Client: client, PClient: pClient}, nil
}

func (o *OSS) ListBuckets(ctx context.Context) ([]types.Bucket, error) {
	result, err := o.Client.ListBuckets(ctx, &s3.ListBucketsInput{})
	if err != nil {
		return nil, err
	}
	return result.Buckets, nil
}

func (o *OSS) BucketExists(ctx context.Context, bucketName string) (bool, error) {
	_, err := o.Client.HeadBucket(ctx, &s3.HeadBucketInput{
		Bucket: aws.String(bucketName),
	})
	exists := true
	if err != nil {
		var apiError smithy.APIError
		if errors.As(err, &apiError) {
			var notFound *types.NotFound
			switch {
			case errors.As(apiError, &notFound):
				exists = false
				err = nil
			}
		}
	}
	return exists, err
}

func (o *OSS) CreateBucket(ctx context.Context, bucketName, region string) error {
	if region == "" {
		region = "us-east-1"
	}
	_, err := o.Client.CreateBucket(ctx, &s3.CreateBucketInput{
		Bucket: aws.String(bucketName),
		CreateBucketConfiguration: &types.CreateBucketConfiguration{
			LocationConstraint: types.BucketLocationConstraint(region),
		},
	})
	return err
}

func (o *OSS) DeleteBucket(ctx context.Context, bucketName string) error {
	_, err := o.Client.DeleteBucket(ctx, &s3.DeleteBucketInput{
		Bucket: aws.String(bucketName),
	})
	return err
}

func (o *OSS) ListObjects(ctx context.Context, bucketName string) ([]types.Object, error) {
	result, err := o.Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
		Bucket: aws.String(bucketName),
	})
	if err != nil {
		return nil, err
	}
	return result.Contents, nil
}

func (o *OSS) PutObject(ctx context.Context, bucketName, objectKey string, body io.Reader) error {
	contentType := mime.TypeByExtension(filepath.Ext(objectKey))
	if contentType == "" {
		contentType = "application/octet-stream"
	}
	_, err := o.Client.PutObject(ctx, &s3.PutObjectInput{
		Bucket:      aws.String(bucketName),
		Key:         aws.String(objectKey),
		Body:        body,
		ContentType: aws.String(contentType),
	})
	return err
}

func (o *OSS) PutLarge(ctx context.Context, bucketName, objectKey string, body io.Reader, partSize int64) error {
	if partSize <= 0 {
		partSize = 8 << 20
	}
	uploader := manager.NewUploader(o.Client, func(u *manager.Uploader) {
		u.PartSize = partSize
	})
	contentType := mime.TypeByExtension(filepath.Ext(objectKey))
	if contentType == "" {
		contentType = "application/octet-stream"
	}
	_, err := uploader.Upload(ctx, &s3.PutObjectInput{
		Bucket:      aws.String(bucketName),
		Key:         aws.String(objectKey),
		Body:        body,
		ContentType: aws.String(contentType),
	})
	return err
}

func (o *OSS) GetObject(ctx context.Context, bucketName, objectKey string) (io.ReadCloser, error) {
	result, err := o.Client.GetObject(ctx, &s3.GetObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String(objectKey),
	})
	if err != nil {
		return nil, err
	}
	return result.Body, nil
}

func (o *OSS) GetLarge(ctx context.Context, bucketName, objectKey string, partSize int64) ([]byte, error) {
	if partSize <= 0 {
		partSize = 8 << 20
	}
	downloader := manager.NewDownloader(o.Client, func(d *manager.Downloader) {
		d.PartSize = partSize
	})
	buffer := manager.NewWriteAtBuffer([]byte{})
	_, err := downloader.Download(ctx, buffer, &s3.GetObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String(objectKey),
	})
	if err != nil {
		return nil, err
	}
	return buffer.Bytes(), nil
}

func (o *OSS) HeadObject(ctx context.Context, bucketName, objectKey string) (*s3.HeadObjectOutput, error) {
	result, err := o.Client.HeadObject(ctx, &s3.HeadObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String(objectKey),
	})
	if err != nil {
		return nil, err
	}
	return result, nil
}

func (o *OSS) DeleteObjects(ctx context.Context, bucketName string, objectKeys ...string) error {
	if len(objectKeys) == 0 {
		return fmt.Errorf("length of object keys must be greater than zero")
	}
	keys := make([]types.ObjectIdentifier, 0, len(objectKeys))
	for _, key := range objectKeys {
		keys = append(keys, types.ObjectIdentifier{Key: aws.String(key)})
	}
	_, err := o.Client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
		Bucket: aws.String(bucketName),
		Delete: &types.Delete{Objects: keys},
	})
	return err
}

func (o *OSS) CopyObject(ctx context.Context, sourceBucket string, destinationBucket string, objectKey, newKey string) error {
	_, err := o.Client.CopyObject(ctx, &s3.CopyObjectInput{
		Bucket:     aws.String(destinationBucket),
		Key:        aws.String(newKey),
		CopySource: aws.String(fmt.Sprintf("%s/%s", sourceBucket, objectKey)),
	})
	return err
}

func (o *OSS) PresignGetObject(ctx context.Context, bucketName, objectKey string, expires time.Duration) (*v4.PresignedHTTPRequest, error) {
	return o.PClient.PresignGetObject(ctx, &s3.GetObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String(objectKey),
	}, func(options *s3.PresignOptions) {
		options.Expires = expires
	})
}

func (o *OSS) GetObjectSignedUrl(ctx context.Context, bucketName, objectKey string, expires time.Duration, redis *Redis) (string, error) {
	if redis == nil {
		r, err := o.PresignGetObject(ctx, bucketName, objectKey, expires)
		if err != nil {
			return "", err
		}
		return r.URL, nil
	}
	re, err := redis.GetOrSingleDo(fmt.Sprintf("oss:signed_url:%s:%s", bucketName, objectKey), func(key string, redis *Redis) (any, error) {
		re, err := o.GetObjectSignedUrl(ctx, bucketName, objectKey, expires, nil)
		if err != nil {
			return re, err
		}
		if _, err = redis.Set(key, re, expires-3*time.Second, ""); err != nil {
			return re, err
		}
		return re, nil
	})
	return redis2.String(re, err)
}

func (o *OSS) PresignPutObject(ctx context.Context, bucketName, objectKey string, expires time.Duration) (*v4.PresignedHTTPRequest, error) {
	contentType := mime.TypeByExtension(filepath.Ext(objectKey))
	if contentType == "" {
		contentType = "application/octet-stream"
	}
	return o.PClient.PresignPutObject(ctx, &s3.PutObjectInput{
		Bucket:      aws.String(bucketName),
		Key:         aws.String(objectKey),
		ContentType: aws.String(contentType),
	}, func(options *s3.PresignOptions) {
		options.Expires = expires
	})
}

func (o *OSS) PresignDeleteObject(ctx context.Context, bucketName, objectKey string, expires time.Duration) (*v4.PresignedHTTPRequest, error) {
	return o.PClient.PresignDeleteObject(ctx, &s3.DeleteObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String(objectKey),
	}, func(options *s3.PresignOptions) {
		options.Expires = expires
	})
}

func (o *OSS) PresignHeadObject(ctx context.Context, bucketName, objectKey string, expires time.Duration) (*v4.PresignedHTTPRequest, error) {
	return o.PClient.PresignHeadObject(ctx, &s3.HeadObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String(objectKey),
	}, func(options *s3.PresignOptions) {
		options.Expires = expires
	})
}
